最後一天。
我原本打算只寫回顧,但翻完這段日子的筆記之後改了主意——有一件事一直
只寫在文件裡,而這三十天講最多次的一句話就是「寫在文件裡的不算數」。
不把它做掉就收尾,整個系列會自己打自己的臉。
sandbox.py 的模組開頭、設計文件的限制清單、app.py 的註解,三個地方
都寫著同一句:
公開部署前必須改成
runner=None,否則任何訪客都能執行任意程式碼。
寫得很清楚,位置也很顯眼。但它的執行方式是:有人在某天記得。
這系統會真的在本機執行學生寫的 Python。擋得住無窮迴圈(逾時中止)與崩潰,
擋不住記憶體耗盡、網路、讀取使用者其他的檔案、fork 炸彈。
兩者的差別不在程式碼,在它跑在哪裡。而程式碼原本對這件事一無所知。
第一個想法是偵測環境——看看是不是跑在 Streamlit Cloud 上,是的話就關掉。
想了五分鐘就否決了。偵測是猜的,而猜錯的代價是遠端任意程式碼執行。
所以反過來:
EXECUTION_ENV_VAR = "CPE_ALLOW_EXECUTION"
def execution_allowed(env=None) -> bool:
"""這台機器允不允許執行學生的程式。"""
if env is None:
env = os.environ
return env.get(EXECUTION_ENV_VAR, "").strip().lower() in {"1", "true", "yes"}
def runner_if_allowed(env=None):
"""允許就給執行器,不允許就給 None(評測會降級成推測)。"""
return run_all if execution_allowed(env) else None
預設不執行,要開得自己明講。 這個方向沒有猜的餘地:忘記設定的後果只是
降級成 LLM 推測——那是安全的失敗方向。
同一個原則也用在值的判讀上:只有 1/true/yes 算開,其他一律當關。
「設了個東西」跟「明確要求執行」是兩回事,拼錯字不該把閘門打開。
-def make_grade_node(client, runner=sandbox.run_all):
+def make_grade_node(client, runner=None):
原本的預設值是「執行」。也就是說,安全與否取決於呼叫端有沒有記得覆寫它。
一個預設值就是一個關於「忘記的時候該怎樣」的決定。而忘記的方向,
不該是執行任意程式碼。
這行改完,app.py 那段長長的警告註解也可以縮短了——它原本在提醒人類,
現在有東西在擋著。
左欄原本在降級時只說一句「以下是推測」。但降級有兩種原因:
畫面上看起來一模一樣,但一個要修、一個不用。所以多了一句:
elif not sandbox.execution_allowed():
st.caption("(這台機器沒有開啟執行功能,所以只能推測。"
"`sandbox` 不是安全沙箱,預設關閉。)")
這是 Day 21 那條原則的又一次應用:看起來一樣但意義不同的兩件事,
要分得出來。
三個 agent,Firestore 當黑板,形成一個閉環:
導師(看程式碼、給引導、記錄弱點)
↓ 寫進 students/{學號}/misconceptions
黑板
↓ 讀出來當挑題依據
出題(從 18 題的題庫挑一題針對弱點的)
↓
評測(真的執行 → 事實;模型解讀失敗原因 → 一句人話)
↓ 全部通過
solved(記下題號,之後不再挑到)→ 導師說一句肯定
.env 整包貼到雲端一樣會破。這是最大的一塊未完成工作。一條限制消失常常換來一條新的(Day 28 學到的)。所以我不寫「已完成」,
寫的是現在卡在哪裡。
一句寫下來的保證,跟一個擋得住的機制,中間的距離就是整個系統的可信度。
寫文件很便宜,寫註解很便宜,在心裡記著更便宜。三十天裡我犯的每一個錯,
幾乎都是把這三種便宜的東西,當成了貴的那種。
而最後一天的改動只有兩個函式、一個預設值——貴的那種,其實也沒那麼貴。
謝謝看到這裡的每一位。